/*-
 * Copyright notice
 * 
 * (c) 2011 Christian Würtz <christian.wuertz@googlemail.com>
 * 
 * All rights reserved.
 * 
 * This file is part of the google-static-map-url-builder project. It is free
 * software: you can redistribute it and/or modify it under the terms of the 
 * GNU General Public License as published by the Free Software Foundation,
 * either version 2 of the License, or (at your option) any later version.
 * 
 * The google-static-map-url-builder is distributed in the hope that it will be
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 
 * Public License for more details.
 *  
 * You should have received a copy of the GNU General Public License
 * along with the google-static-map-url-builder. If not, see 
 * <http://www.gnu.org/licenses/>.
 */
package de.wuertz.map.url;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.wuertz.map.enumeration.ImageFormat;
import de.wuertz.map.enumeration.MapType;
import de.wuertz.map.exception.InvalidParameterException;
import de.wuertz.map.exception.URLOverlengthException;
import de.wuertz.map.types.Coordinate;
import de.wuertz.map.types.Marker;
import de.wuertz.map.types.Path;

/**
 * This object generates URLs to access Google's static maps service. This service offers access to
 * Google Maps images.
 * 
 * More detailed information can be found at
 * http://code.google.com/apis/maps/documentation/staticmaps/
 * 
 * TODO: Unsupported parameters: language, visible, style
 * 
 * @author Christian Würtz <christian.wuertz@googlemail.com>
 */
public class GoogleStaticMapsUrlGenerator
{
	/**
	 * Logger.
	 */
	static final Logger LOG = LoggerFactory.getLogger( GoogleStaticMapsUrlGenerator.class );

	/**
	 * This is the basic part of the service's URL.
	 */
	private static final String BASE_URL = "http://maps.googleapis.com/maps/api/staticmap?";

	/**
	 * (Required, if markers aren't present)
	 * 
	 * The center of the map.
	 */
	private String center;

	/**
	 * (Required, if markers aren't present)
	 * 
	 * The zoom level.
	 */
	private short zoomLevel;

	/**
	 * (Required)
	 * 
	 * The height of the map.
	 */
	private int height;

	/**
	 * (Required)
	 * 
	 * The width of the map.
	 */
	private int width;

	/**
	 * (Optional)
	 * 
	 * The scale of the map.
	 */
	private short scale;

	/**
	 * (Optional)
	 * 
	 * The image format of the map.
	 */
	private ImageFormat format;

	/**
	 * (Optional)
	 * 
	 * The type of the map.
	 */
	private MapType mapType;

	/**
	 * (Optional)
	 * 
	 * A list of markers.
	 */
	private ArrayList<Marker> markers;

	/**
	 * (Optional)
	 * 
	 * A list of paths.
	 */
	private ArrayList<Path> paths;

	/**
	 * Indicate the user's position by a sensor like GPS.
	 */
	private boolean sensor;

	/**
	 * Private constructor.
	 */
	public GoogleStaticMapsUrlGenerator()
	{
		// Initialize some objects.
		this.markers = new ArrayList<Marker>();
		this.paths = new ArrayList<Path>();
	}

	/**
	 * Get the URL.
	 * 
	 * @return The URL to the map.
	 * 
	 * @throws MalformedURLException
	 * @throws URLOverlengthException
	 * @throws InvalidParameterException
	 */
	public URL generateURL() throws MalformedURLException, URLOverlengthException,
			InvalidParameterException
	{
		// Initialize the builder of the string that contains the URL with the maximum URL size
		// allowed by the Google static maps API.
		StringBuilder builder = new StringBuilder( 2500 );
		builder.append( BASE_URL );

		// Check whether the width and height of the map have been specified.
		if( this.width <= 0 || this.height <= 0 )
		{
			throw new InvalidParameterException( "The dimensions must be bigger than zero." );
		}

		builder.append( "&" );
		builder.append( "size=" );
		builder.append( this.width );
		builder.append( "x" );
		builder.append( this.height );

		// Check whether a center was specified. This is necessary, if markers aren't present.
		if( this.markers.isEmpty() && this.paths.isEmpty() && this.center == null )
		{
			throw new InvalidParameterException(
					"The center must be specified, if no marker's are available." );
		}
		else if( this.center != null )
		{
			builder.append( "&" );
			builder.append( "center=" );
			builder.append( this.center );
		}

		// Set the zoom level.
		builder.append( "&" );
		builder.append( "zoom=" );
		builder.append( this.zoomLevel );

		// Check whether a scale has been specified. If not or if it's one, don't add anything to the
		// URL, because the default value is one and thus characters can be saved.
		if( this.scale != 0 && this.scale != 1 )
		{
			builder.append( "&" );
			builder.append( "scale=" );
			builder.append( this.scale );
		}

		// Set the file format of the map, if it's not the default value.
		if( this.format != null && this.format != ImageFormat.PNG8 )
		{
			builder.append( "&" );
			builder.append( "format=" );
			builder.append( this.format.toString() );
		}

		// Set the file format of the map, if it's not the default value.
		if( this.mapType != null && this.mapType != MapType.ROADMAP )
		{
			builder.append( "&" );
			builder.append( "maptype=" );
			builder.append( this.mapType.toString() );
		}

		// Add the paths.
		for( Path curPath : this.paths )
		{
			builder.append( curPath.getURLRepresentation() );
		}

		// Add the markers.
		for( Marker curMarker : this.markers )
		{
			builder.append( curMarker.getURLRepresentation() );
		}

		// Set the sensor flag.
		builder.append( "&" );
		builder.append( "sensor=" );
		builder.append( this.sensor );

		// Check whether the size of the URL is allowed.
		String url = builder.toString();
		if( url.length() > 2500 )
		{
			throw new URLOverlengthException();
		}

		return new URL( builder.toString() );
	}

	/**
	 * Get the center of the map.
	 * 
	 * @return The center of the map as coordinate or address.
	 */
	public String getCenter()
	{
		return this.center;
	}

	/**
	 * (Required, if markers aren't present)
	 * 
	 * Set the center of the map.
	 * 
	 * @param center The coordinate of the center of the map.
	 */
	public void setCenter( Coordinate center )
	{
		this.center = center.toString();
	}

	/**
	 * (Required, if markers aren't present)
	 * 
	 * Set the center of the map.
	 * 
	 * @param address An address (e.g. "Berkeley,CA" or "City Hall,New York,NY") or a coordinate in
	 *           it's string representation. If an address is passed, spaces will be escaped by plus
	 *           signs.
	 */
	public void setCenter( String address )
	{
		this.center = address.replace( " ", "+" );
	}

	/**
	 * Get the zoom level.
	 * 
	 * @return The zoom level.
	 */
	public short getZoomLevel()
	{
		return this.zoomLevel;
	}

	/**
	 * (Required, if markers aren't present)
	 * 
	 * Set the zoom level. It must be between 0 (the lowest zoom level, in which the entire world can
	 * be seen on the map) to 21 (the highest zoom level, in which individual buildings can be seen
	 * on the map).
	 * 
	 * Each succeeding zoom level doubles the precision in both horizontal and vertical dimensions.
	 * 
	 * Note: not all zoom levels are available at all locations on the earth. If a region doesn't
	 * support the requested zoom level, the Google static maps API will return a blank image instead
	 * of a map.
	 * 
	 * @param zoomLevel The zoom level.
	 * 
	 * @throws InvalidParameterException
	 */
	public void setZoomLevel( short zoomLevel ) throws InvalidParameterException
	{
		// If the zoom isn't within the allowed range, throw an invalid parameter exception.
		if( ( zoomLevel >= 0 && zoomLevel <= 21 ) )
		{
			this.zoomLevel = zoomLevel;
		}
		else
		{
			throw new InvalidParameterException( "The zoom level must be between 0 and 21." );
		}
	}

	/**
	 * Get the height of the map.
	 * 
	 * @return The height of the map.
	 */
	public int getHeight()
	{
		return this.height;
	}

	/**
	 * Get the width of the map.
	 * 
	 * @return The width of the map.
	 */
	public int getWidth()
	{
		return this.width;
	}

	/**
	 * (Required)
	 * 
	 * Set the dimensions of the map. Depending on the scale free and business users can use
	 * different maximal values. If bigger values are given, the Google static maps API will reduce
	 * them to the maximal value.
	 * 
	 * <pre>
	 *  										scale=1 		scale=2 											scale=4 
	 * Free 									640x640 		640x640 (returns 1280x1280 pixels) 		Not available. 
	 * Google Maps API for Business 	2048x2048 	1024x1024 (returns 2048x2048 pixels) 	512x512 (returns 2048x2048 pixels)
	 * </pre>
	 * 
	 * @param width The width of the map in pixel.
	 * @param height The height of the map in pixel.
	 * 
	 * @throws InvalidParameterException
	 */
	public void setMapDimensions( int width, int height ) throws InvalidParameterException
	{
		// Check whether the dimensions are positive values bigger than 0.
		if( width > 0 && height > 0 )
		{
			this.width = width;
			this.height = height;
		}
		else
		{
			throw new InvalidParameterException( "The dimensions must be bigger than zero." );
		}
	}

	/**
	 * Get the scale of the map
	 * 
	 * @return
	 */
	public short getScale()
	{
		return this.scale;
	}

	/**
	 * (Optional)
	 * 
	 * The scale affects the number of pixels that are returned. For example a scale of two returns
	 * twice as many pixels as a scale of one, while retaining the same coverage area and level of
	 * detail (i.e. the contents of the map don't change).
	 * 
	 * This is useful when developing for high-resolution displays, or when generating a map for
	 * printing.
	 * 
	 * The default value is 1. Other accepted values are 2 and 4. A value of 4 is only available for
	 * business customers.
	 * 
	 * @param scale The scale of the map to set.
	 * 
	 * @throws InvalidParameterException
	 */
	public void setScale( short scale ) throws InvalidParameterException
	{
		// If the scale isn't within the allowed values, throw an invalid parameter exception.
		switch( scale )
		{
			case 1:
				this.scale = 1;
				break;
			case 2:
				this.scale = 2;
				break;
			case 4:
				this.scale = 4;
				break;
			default:
				throw new InvalidParameterException( "Only scales of 1, 2 or 4 are supported." );
		}
	}

	/**
	 * Get the image format.
	 * 
	 * @return The image format.
	 */
	public ImageFormat getFormat()
	{
		return this.format;
	}

	/**
	 * Set the image format.
	 * 
	 * @param format The image format to set.
	 */
	public void setFormat( ImageFormat format )
	{
		this.format = format;
	}

	/**
	 * Get the type of the map.
	 * 
	 * @return The type of the map.
	 */
	public MapType getMapType()
	{
		return this.mapType;
	}

	/**
	 * Set the type of the map.
	 * 
	 * @param mapType The type of the map to set.
	 */
	public void setMapType( MapType mapType )
	{
		this.mapType = mapType;
	}

	/**
	 * Get the list of the markers.
	 * 
	 * @return The list of the markers.
	 */
	public ArrayList<Marker> getMarkers()
	{
		return this.markers;
	}

	/**
	 * Add a marker to the map.
	 * 
	 * @param marker The marker to add to the map.
	 */
	public void addMarker( Marker marker )
	{
		this.markers.add( marker );
	}

	/**
	 * Get the list of paths.
	 * 
	 * @return The list of paths.
	 */
	public ArrayList<Path> getPaths()
	{
		return this.paths;
	}

	/**
	 * Add a path to the map.
	 * 
	 * @param path The path to add to the map.
	 */
	public void addPath( Path path )
	{
		this.paths.add( path );
	}

	/**
	 * Check whether a sensor should be used to indicate the user's position.
	 * 
	 * @return True, if a sensor should be used. Otherwise, false.
	 */
	public boolean isSensorInUse()
	{
		return this.sensor;
	}

	/**
	 * Set the sensor flag.
	 * 
	 * @param sensor True, if a sensor should be used. Otherwise, false.
	 */
	public void setSensor( boolean sensor )
	{
		this.sensor = sensor;
	}
}
